Skip to content

feat(classroom): みんなの課題 — 全国の先生による課題共有ライブラリ (EPIC #1066)#1080

Open
smalruby3-editor-bot[bot] wants to merge 27 commits into
developfrom
feature/assignment-sharing
Open

feat(classroom): みんなの課題 — 全国の先生による課題共有ライブラリ (EPIC #1066)#1080
smalruby3-editor-bot[bot] wants to merge 27 commits into
developfrom
feature/assignment-sharing

Conversation

@smalruby3-editor-bot

Copy link
Copy Markdown

Summary

EPIC #1066「みんなの課題」の統合 PR。先生が作成した課題(説明ページ + スタータープロジェクト + 補足資料 URL)を全国の先生に共有・再利用できるライブラリ。設計の正典はスパイク #1067 の Decision Log(D1〜D12、壁打ち 2 ラウンドで確定)。

Closes #1068, closes #1069, closes #1070, closes #1071, closes #1072, closes #1066

構成(sub-issue → PR、トピックブランチへマージ済み)

# 内容 PR
S1 #1068 [backend/infra] 共有ストア(テーブル×2・専用バケット)+ API 7本 #1075
S2 #1069 [frontend] 共有フォーム(属性・URL ガイダンス・プロフィール記憶・CC BY 同意) #1076
S3 #1070 [frontend] カタログ(絞り込み・詳細・取り込み・自分の投稿・通報) #1077
S4 #1071 [ops] 通報対応 CLI + runbook(D3 事後対応型) #1078
S5 #1072 [docs/E2E] feature docs + E2E + stg 実機検証 #1079

主要な設計判断(詳細は EPIC の Decision Log)

  • D1 閲覧・投稿とも先生ログイン必須 / D2 CC BY 4.0 統一(投稿時同意・クレジット=表示名)
  • D4 補足 URL: 期待内容(指導案・スライド等)を入力欄に明示 + https 必須 + 閲覧側は外部ドメイン名付き確認
  • D6 プロフィール最小化: 表示名 + 任意の所属のみ(メール・実名不保持)
  • D7 共有データは永続(TTL 対象外・prod は RETAIN + PITR・専用バケットで classroom lifecycle と分離)
  • D11/D12: スターター 50MB 上限・共有 10件/日・通報 20件/日

Test Coverage

  • Lambda unit 233 件 pass(shared-assignments 25 / shared-admin-lib 6 を追加)、tsc / cdk synth クリーン
  • GUI unit: 共有フォーム 7 / カタログ 9 / タクソノミ 4 / プロフィール 4、eslint(--max-warnings 0)/ prettier / i18n parity(ja 358 = ja-Hira 358)
  • E2E(stg 実機・2 ロケール PASS): 共有 → カタログ絞り込み → 詳細(CC BY クレジット・外部リンク確認)→ 取り込み → 取り下げ/再公開

デプロイ状況

  • stg: デプロイ・検証済み(テーブル・バケット・ルートすべて生成確認、E2E PASS)
  • prod: 未デプロイ(API は追加のみで後方互換 — 本 PR マージ前に先行デプロイ可能)
  • フロントは本 PR マージ → CI リリースで公開

⚠️ 人間確認事項

  • プライバシーポリシー / 利用規約: 共有機能で「表示名・所属表記・課題内容が全国の先生に公開される」旨と CC BY 4.0 ライセンスの記載が必要(D2/D6 関連。クラス機能の既存記載とは別項目)
  • 「みんなの課題」の名称・CC BY 同意文言・補足 URL ガイダンス文言の最終確認

🤖 Generated with Claude Code

smalruby3-editor-bot Bot and others added 10 commits July 18, 2026 02:26
みんなの課題 (EPIC #1066, design: spike #1067 D1-D12):

- new SharedAssignments table (no TTL; prod RETAIN + PITR; GSIs
  status-createdAt / authorSub-createdAt) and SharedAssignmentReports
  table (90-day TTL)
- new smalruby-shared-assignments bucket (no lifecycle = permanent,
  prod RETAIN) so the classroom retention sweep never touches shares
- 7 teacher-auth endpoints: share (snapshot copy into the shared
  bucket, CC BY consent required, 50MB starter cap, 10/day quota),
  newest-first filtered catalog with cursor pagination + mine=1,
  detail with presigned URLs, import into own class (reverse copy +
  reuseCount), author-only update (overwrite re-snapshot) / unlist,
  and report (20/day quota, reason required)
- responses never expose authorSub / reporterSub; quotas reuse the
  eval-quota atomic counter pattern
- docs: architecture.md route table + data notes; .env.example limits

EPIC #1066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(classroom-api): みんなの課題 — 共有ストア・API 一式 (S1 #1068)
みんなの課題 S2 (EPIC #1066, design: spike #1067):

- "Share this assignment" button in the assignment detail opens an
  inline form: title/summary, school level x grades x subject
  (controlled vocabulary, free text for "other" — D5), free tags,
  lesson count, supplement URL with explicit guidance on what belongs
  there + client-side https check (D4), minimal author profile
  remembered in localStorage smalruby:sharedAuthorProfile (D6), and a
  mandatory CC BY 4.0 consent checkbox (D2)
- publish confirmation shows the CC BY credit line
- classroom-api gains the 7 shared-assignment methods (share / list /
  detail / import / update / unlist / report) for S2+S3
- new libs: shared-assignment-taxonomy (mirrors the server vocabulary)
  and shared-author-profile (SSR-guarded persistence)
- new hook use-shared-assignments wired through the teacher aggregator
- docs: ui-ux.md / testing.md testids

EPIC #1066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(classroom): みんなの課題 — 共有フォーム (S2 #1069)
みんなの課題 S3 (EPIC #1066, design: spike #1067):

- "Find in みんなの課題" on the assignment board opens the catalog:
  newest-first cards (attribute badges, tags, author, reuse count)
  with school-level/subject/grade/tag filters and cursor pagination
- detail preview shows the snapshot pages, the CC BY 4.0 credit line
  and the supplement URL behind an explicit external-domain
  confirmation (D4); import copies the assignment (starter included)
  into the current class and surfaces the new join code note
- "My posts" tab lists the caller's own items (incl. unlisted) with
  withdraw / republish; strangers get a report flow (reason required)
- use-shared-assignments grows the catalog state machine and refreshes
  the board after an import
- docs: ui-ux.md / testing.md testids

EPIC #1066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(classroom): みんなの課題 — カタログ・取り込み UI (S3 #1070)
みんなの課題 S4 (EPIC #1066 D3, post-moderation):

- bin/shared-assignments-admin.ts: list-reports (report queue grouped
  by item, most-reported first), show <sharedId>, unpublish/republish
  (dry-run by default, --apply to execute; never hard-deletes)
- planning/rendering logic split into lambda/shared-admin-lib.ts
  (pure, unit-tested); reporterSub is never rendered
- docs/assignment-sharing/operations.md: moderation runbook — flow,
  judgment criteria, the no-contact-address premise (D6), and the
  future hand-off to the Admin SPA (EPIC #1073)

EPIC #1066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(classroom-ops): みんなの課題 — 通報対応 CLI + runbook (S4 #1071)
みんなの課題 S5 (EPIC #1066):

- docs/assignment-sharing/README.md: feature overview (D1-D12 recap,
  share/catalog flow diagram, file inventory, settings) with Japanese
  UI screenshots 0101-0104 captured headless against the deployed stg
  API; classroom README links across
- tools/playwright-verify/verify-assignment-sharing.mjs: end-to-end
  pass — create class/assignment with content, share (CC BY consent,
  publish confirmation carries the credit), catalog filter, detail
  (credit line + external-domain URL confirmation), import into the
  class (new board row), my-posts unlist/republish, stg cleanup
- verified on stg: full run PASS twice (en + ja locales)

EPIC #1066

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(assignment-sharing): みんなの課題 — docs/E2E/stg 検証 (S5 #1072)
@github-actions

Copy link
Copy Markdown

…list

DynamoDB rejects an empty ExpressionAttributeNames map, so
GET /shared-assignments?mine=1 (自分の投稿タグ) always returned 500 when no
attribute filters were set — the query needs no #status alias in mine mode.
Build the map first and spread the key in only when non-empty (same pattern
as the group update at handler.ts:2591).

Found during PR #1080 review on prod (2026-07-18).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(classroom): mine=1 の shared list が空 ExpressionAttributeNames で 500
@smalruby3-editor-bot

Copy link
Copy Markdown
Author

レビュー中に発見された 500 の修正と DoD 結果 (2026-07-18)

症状と原因

  • 症状: クラス一覧 > 課題一覧 > みんなの課題からさがす > 自分の投稿GET /shared-assignments?mine=1 が 500(投稿の有無に関係なく常に発生)
  • 原因: mine=1 は KeyCondition に #status エイリアスが不要なため、属性フィルタ未指定時に ExpressionAttributeNames が空マップ {} となり、DynamoDB が ValidationException: ExpressionAttributeNames must not be empty で拒否(prod CloudWatch で確認)
  • 修正: fix(classroom): mine=1 の shared list が空 ExpressionAttributeNames で 500 #1090(本ブランチにマージ済み)— 非空のときだけキーを渡す。同パターンの横断監査済みで該当はこの 1 箇所のみ

stg 検証

  • npx jest lambda/tests/handler-shared-assignments.test.ts 25 pass(mine=1 に「names が undefined」のアサーション追加)
  • curl 直叩き ?mine=1 → 200
  • headless E2E(tools/playwright-verify/verify-assignment-sharing.mjs)PASS
  • headful 本格動作確認(localhost:8601 → stg API、実 Google アカウント): 課題作成(テキスト2ページ+画像+スターター .sb3)→ 共有(タクソノミー/補足URL/CC BY 同意)→ カタログ検索・詳細(クレジット/S3画像/外部リンク確認)→ クラスへ取り込み(ページ・画像・スターター複製+新参加コード)→ 自分の投稿(投稿ゼロ時の空表示/投稿あり時の一覧/取り下げ→再公開)→ 通報(自分の投稿にはボタン非表示・他人の投稿で送信成功)まで全項目パス。テストデータはクリーンアップ済み

prod デプロイ + プレビュー URL での DoD

  • ClassroomStack UPDATE_COMPLETE(diff は Lambda コードのみ)
  • ブランチプレビュー https://smalruby.jp/smalruby3-editor/feature/assignment-sharing/ にて実 Google アカウント(投稿ゼロ = 元の再現条件)でログインし、自分の投稿タブ → 正常な空表示「共有された課題が見つかりませんでした。」
  • ブラウザ実測: GET https://classroom.api.smalruby.app/shared-assignments?mine=1200

🤖 Generated with Claude Code

takaokouji and others added 10 commits July 19, 2026 12:37
…-sharing

# Conflicts:
#	tools/playwright-verify/README.md
課題詳細に「課題を配信(GC)」と「この課題を共有(みんなの課題)」が併存し、
さらに参加コードが場所を取って全画面ボタンも別にあり、それぞれ何をするのか
分かりにくかった。参加コード周りを1枚のカードに整理:

- 参加コードカード: 「参加コード:xxxxxx」+ アクション行
  (全画面表示 / 招待リンクをコピー / Google Classroom に参加コードを共有)
- 課題名の横にあった「課題を配信」ボタンを廃止し、カード内の
  「Google Classroom に参加コードを共有」へ移動・改称(何をするか一目でわかる。
  配信済みなら「Google Classroom で開く」)。GC 未連携なら非表示
- 招待リンクのコピーを詳細にも追加(従来は全画面表示の中だけ)

ja.js / ja-Hira.js に joinCode.shareToGc / openInGc を対で追加。
みんなの課題の「この課題を共有」ボタンは (2) で一覧へ移すため今回は現状維持。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
参加コードカードが縦に数行になり全体レイアウトが崩れていたのを、
課題名(左)と参加コード(右)の均等幅2カラムに分割し各カラムを約2行に整理:

- 左: 課題名を2行の textarea に(長い課題名が折り返して全文見える。
  Enter は改行せず保存し単一行の値を維持)
- 右: 行1=参加コード + 全画面表示 / 行2=招待リンクをコピー + GC共有
- Google Classroom 共有ボタンは狭い幅では "Google Classroom" を省いて
  "[アイコン]に参加コードを共有" 表示(カード幅で判定する container query)。
  shareToGcShort / openInGcShort を ja/ja-Hira 対で追加

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
人数変更の不具合修正:
- クラス(グループ)の studentCount 変更が既存課題に反映されなかった。
  handleUpdateGroup で変更時に配下のアクティブな課題へ伝播する
  (teacherSub-index + groupId フィルタで列挙し各課題の studentCount を更新)。
  増やす=席追加(安全)、減らす=席削除
- フロント: 人数を減らすときだけ警告(「N人に減らすと M+1〜元 番の席が
  なくなり提出作品が表示されなくなります」→ 減らして保存 / 人数はそのまま)。
  増やすときは警告なし

課題詳細レイアウト洗練 (割り込み要望):
- 課題名: ラベルと2行フィールドを横並び・ラベルは縦センタリング
- 参加コード: 値のフォントをラベルと同サイズに(ボールド・色は維持)
- 各ボタンを最初から flex-wrap 配置(狭ければ2行折返し)。狭幅では
  全画面表示・招待リンクコピーをアイコンのみに、Google Classroom 共有は
  "Google Classroom" を省略(カード幅の container query で切替)

テスト: infra 伝播 3(増/減/フィルタ)、GUI 減少警告 3。ja/ja-Hira 対で
文言追加。ブラウザで全レイアウト・警告・狭幅アイコン化を確認。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
課題詳細の参加コードカードを次のように改善:

- コード値と3つのボタン(全画面表示/招待リンクコピー/GC共有)を1つの
  flex flow にまとめ、同じ行から始めることで3行になるのを防ぐ
- 課題名カラムと参加コードカードを flex-wrap で並べ、幅が足りず
  カードが340pxを割り込むと参加コードカードを単独行(フル幅)に
  折り返して狭いウインドウでも2行に収める
- カード幅が狭いとき(container query)は全画面表示/コピーをアイコン
  のみに、Google Classroom共有を短縮表示にする
- 参加コードの値のフォントを課題名ラベルと同じ1remに拡大(色・太字は維持)
- 課題名入力の高さを参加コードカードに一致させる(align-items: stretch)
- 「Google Classroom に参加コードを共有」を「Google Classroom に共有」、
  短縮形を「に共有」に変更

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ラベルが長くレイアウトが崩れやすい問題を、ボタンを幅に依らず
コンパクトに固定して解決:

- 全画面表示・招待リンクコピーは幅に関わらずアイコンのみ(tooltip で
  ラベルを補足)
- 招待リンクコピーはクリックでアイコンがチェックに変わり緑で強調、
  コピー完了が分かるリアクションを追加
- Google Classroom 共有は幅に関わらず「[アイコン]に共有」の短縮表示で固定
- 幅で表示を切り替える container query と未使用の全文ロケールキー
  (shareToGc / openInGc) を削除

これにより多くの幅で参加コードカードが1行に収まり、狭くても2行を保つ。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
保存期限まわりの表示を改善し、限られた画面領域を有効活用する(#1052/#1049):

課題詳細:
- 専用の警告バナーを廃止し、保存期限の右に警告メッセージをインライン表示
  「保存期限: 2026/7/21 ⚠保存期限をすぎるとこの課題と提出物は自動削除
  されます。ダウンロードして保存してください」。バナーを消した分、下の
  編集領域が広がる
- 幅が狭いときは「ダウンロードして保存してください」を省略(container query)
- 期限が近いとき全作品ダウンロードボタンを警告色(赤)にしてDLを促す

課題一覧(ボード):
- 「あと N 日」バッジをやめ、課題詳細と同じ「この課題と提出物は {日付} に
  自動削除されます。ダウンロードして保存してください。」を行に表示
- 各行に全作品ダウンロードボタンを追加(その課題だけをZIP化。既存の
  handleDownloadClassAll を単一課題で再利用)。DL中の行だけ進捗を表示

未使用になった board.expiryBadge ロケールキーを削除。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Google Classroom 配信画面をクラス管理の基本レイアウトに揃える:

配信フォーム:
- パンくず(クラス一覧 > 課題一覧 > 課題詳細 > Google Classroom に共有)を
  親から表示。フェーズ内の「< 戻る」ボタンは廃止
- 「対象:」ラベルを廃止し、クラス名を課題詳細と同じ表記(例: 技術 2026年度、
  formatClassLabel)でサブタイトル表示
- フッターを基本ポリシーに統一: キャンセル(左下・戻ると同じ挙動)+
  プライマリー「Google Classroom に配信する」(右下・必須未入力で非活性)

配信後の画面:
- 中央寄せの旧レイアウトをやめ、基本レイアウトに統一。パンくずを維持し、
  「✓ 配信しました!」+ ヒント + フッター(左=課題詳細にもどる /
  右=Google Classroom で確認する)

未使用の postAssignment.target ロケールキーを削除し、breadcrumbs.shareGc /
postAssignment.cancel / postAssignment.backToDetail を追加。
teacher-post-assignment の単体テストを追加。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
配信画面の本文ルートが未定義の .phaseContainer を使っており左右パディングが
効いていなかった。課題詳細(.detail-layout)と同じ padding: 0 2rem 1.25rem を
持つ .post-assignment-container に置き換え、パンくず・タイトル・本文の左端を
2rem に揃える。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
課題詳細のクラス名(.teacher-view-title / div)が太字になっておらず、課題一覧
の見出し(h2.board-title は既定で太字)と見た目が揃っていなかった。
.teacher-view-title に font-weight: bold を追加して統一する(評価画面の
同クラス見出しも同様に揃う)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
smalruby3-editor-bot Bot and others added 2 commits July 20, 2026 14:44
みんなの課題(#1066)に限定公開パイプラインの土台を追加:

- shared-assignment に visibility('public'|'limited') と passcode(合言葉)を導入。
  #1109 以前の項目は visibility 無し=public とみなす(後方互換)
- 限定公開の発行は軽い検証(CC BY 同意・属性・著者名は任意)で、参加コード同型の
  合言葉を発行。完璧さの圧を下げる
- 公開カタログ(GET /shared-assignments)は限定公開を FilterExpression で除外。
  mine=1 は自分の合言葉を含める
- 合言葉のルックアップ(POST /shared-assignments/lookup)と取り込み
  (POST /shared-assignments/import-by-passcode)を追加。sharedId は非著者に露出しない
- GET /{id} は限定公開を非著者から 404 で隠す
- PATCH で限定公開→全体公開に広げるとき、CC BY 同意・属性・著者名を必須化
- SharedAssignments に passcode-index GSI を追加

unit テスト 8 件追加(33 件 pass)。tsc / cdk synth(stg)通過。deploy は未実施。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
classroom-api.js に限定公開(合言葉)用の 2 メソッドを追加:
- lookupSharedByPasscode: 取り込み前のプレビュー(sharedId 非露出)
- importSharedByPasscode: 合言葉で自分のクラスに取り込み

shareAssignment は payload に visibility を渡せる(backend が分岐)。UI 配線は後続。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
smalruby3-editor-bot Bot and others added 3 commits July 20, 2026 15:00
みんなの課題の API 表に visibility(public/limited)・合言葉ルックアップ
(/shared-assignments/lookup)・合言葉取り込み(/import-by-passcode)・
限定公開の除外/隠蔽/昇格の挙動・passcode-index GSI を反映。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
出す側の共有導線を「課題詳細タブ行」から「課題一覧(ボード)の各行」に一本化:

- ボード各行に「共有」ボタン → 共有設定ステップ(深さは クラス > 課題 > 共有)
- 共有設定ステップ(teacher-share-step.jsx)で公開範囲を選ぶ:
  - 限定公開(合言葉): タイトルのみで発行。合言葉を大きく表示+コピー。研究授業で
    少し試した課題でも出せる(完璧さの圧を下げる)。あとで全体公開へ広げられる
  - 全体公開(みんなの課題): 既存の SharedAssignmentForm を再利用
- 課題詳細タブ行の「この課題を共有」ボタン + インラインフォームは廃止
- use-shared-assignments: showShareForm → shareTarget(対象課題)に変更。発行後は
  ステップに留まり結果(合言葉)を確認できる
- 不要になった shared.openForm ロケールキーを削除

unit テスト追加(share-step 5件・board 共有導線 3件)。lint / prettier / 関連テスト green。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
限定公開の課題を合言葉で内輪取り込みする導線を追加:

- ボードのアクションバーに「合言葉で取り込み」ボタン
- teacher-passcode-import.jsx: 合言葉入力 → 確認(lookup・sharedId 非露出)→
  プレビュー(課題名)→「このクラスに取り込む」で組に取り込み
- use-shared-assignments: passcode の lookup / import 状態とハンドラを追加
- 共有ステップと同様、取り込み中はボードのアクションバーを隠す

unit テスト追加(3件)。lint / prettier / 関連テスト green。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment